// Copyright 2018 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "src/inspector/custom-preview.h"

#include "src/debug/debug-interface.h"
#include "src/inspector/injected-script.h"
#include "src/inspector/inspected-context.h"
#include "src/inspector/string-util.h"
#include "src/inspector/v8-console-message.h"
#include "src/inspector/v8-inspector-impl.h"
#include "src/inspector/v8-stack-trace-impl.h"

namespace v8_inspector {

using protocol::Runtime::CustomPreview;

namespace {
    void reportError(v8::Local<v8::Context> context, const v8::TryCatch& tryCatch)
    {
        DCHECK(tryCatch.HasCaught());
        v8::Isolate* isolate = context->GetIsolate();
        V8InspectorImpl* inspector = static_cast<V8InspectorImpl*>(v8::debug::GetInspector(isolate));
        int contextId = InspectedContext::contextId(context);
        int groupId = inspector->contextGroupId(contextId);
        v8::Local<v8::String> message = tryCatch.Message()->Get();
        v8::Local<v8::String> prefix = toV8String(isolate, "Custom Formatter Failed: ");
        message = v8::String::Concat(isolate, prefix, message);
        std::vector<v8::Local<v8::Value>> arguments;
        arguments.push_back(message);
        V8ConsoleMessageStorage* storage = inspector->ensureConsoleMessageStorage(groupId);
        if (!storage)
            return;
        storage->addMessage(V8ConsoleMessage::createForConsoleAPI(
            context, contextId, groupId, inspector,
            inspector->client()->currentTimeMS(), ConsoleAPIType::kError, arguments,
            String16(), nullptr));
    }

    void reportError(v8::Local<v8::Context> context, const v8::TryCatch& tryCatch,
        const String16& message)
    {
        v8::Isolate* isolate = context->GetIsolate();
        isolate->ThrowException(toV8String(isolate, message));
        reportError(context, tryCatch);
    }

    InjectedScript* getInjectedScript(v8::Local<v8::Context> context,
        int sessionId)
    {
        v8::Isolate* isolate = context->GetIsolate();
        V8InspectorImpl* inspector = static_cast<V8InspectorImpl*>(v8::debug::GetInspector(isolate));
        InspectedContext* inspectedContext = inspector->getContext(InspectedContext::contextId(context));
        if (!inspectedContext)
            return nullptr;
        return inspectedContext->getInjectedScript(sessionId);
    }

    bool substituteObjectTags(int sessionId, const String16& groupName,
        v8::Local<v8::Context> context,
        v8::Local<v8::Array> jsonML, int maxDepth)
    {
        if (!jsonML->Length())
            return true;
        v8::Isolate* isolate = context->GetIsolate();
        v8::TryCatch tryCatch(isolate);

        if (maxDepth <= 0) {
            reportError(context, tryCatch,
                "Too deep hierarchy of inlined custom previews");
            return false;
        }

        v8::Local<v8::Value> firstValue;
        if (!jsonML->Get(context, 0).ToLocal(&firstValue)) {
            reportError(context, tryCatch);
            return false;
        }
        v8::Local<v8::String> objectLiteral = toV8String(isolate, "object");
        if (jsonML->Length() == 2 && firstValue->IsString() && firstValue.As<v8::String>()->StringEquals(objectLiteral)) {
            v8::Local<v8::Value> attributesValue;
            if (!jsonML->Get(context, 1).ToLocal(&attributesValue)) {
                reportError(context, tryCatch);
                return false;
            }
            if (!attributesValue->IsObject()) {
                reportError(context, tryCatch, "attributes should be an Object");
                return false;
            }
            v8::Local<v8::Object> attributes = attributesValue.As<v8::Object>();
            v8::Local<v8::Value> originValue;
            if (!attributes->Get(context, objectLiteral).ToLocal(&originValue)) {
                reportError(context, tryCatch);
                return false;
            }
            if (originValue->IsUndefined()) {
                reportError(context, tryCatch,
                    "obligatory attribute \"object\" isn't specified");
                return false;
            }

            v8::Local<v8::Value> configValue;
            if (!attributes->Get(context, toV8String(isolate, "config"))
                     .ToLocal(&configValue)) {
                reportError(context, tryCatch);
                return false;
            }

            InjectedScript* injectedScript = getInjectedScript(context, sessionId);
            if (!injectedScript) {
                reportError(context, tryCatch, "cannot find context with specified id");
                return false;
            }
            std::unique_ptr<protocol::Runtime::RemoteObject> wrapper;
            protocol::Response response = injectedScript->wrapObject(originValue, groupName, WrapMode::kNoPreview,
                configValue, maxDepth - 1, &wrapper);
            if (!response.isSuccess() || !wrapper) {
                reportError(context, tryCatch, "cannot wrap value");
                return false;
            }
            v8::Local<v8::Value> jsonWrapper;
            String16 serialized = wrapper->toJSON();
            if (!v8::JSON::Parse(context, toV8String(isolate, serialized))
                     .ToLocal(&jsonWrapper)) {
                reportError(context, tryCatch, "cannot wrap value");
                return false;
            }
            if (jsonML->Set(context, 1, jsonWrapper).IsNothing()) {
                reportError(context, tryCatch);
                return false;
            }
        } else {
            for (uint32_t i = 0; i < jsonML->Length(); ++i) {
                v8::Local<v8::Value> value;
                if (!jsonML->Get(context, i).ToLocal(&value)) {
                    reportError(context, tryCatch);
                    return false;
                }
                if (value->IsArray() && value.As<v8::Array>()->Length() > 0 && !substituteObjectTags(sessionId, groupName, context, value.As<v8::Array>(), maxDepth - 1)) {
                    return false;
                }
            }
        }
        return true;
    }

    void bodyCallback(const v8::FunctionCallbackInfo<v8::Value>& info)
    {
        v8::Isolate* isolate = info.GetIsolate();
        v8::TryCatch tryCatch(isolate);
        v8::Local<v8::Context> context = isolate->GetCurrentContext();
        v8::Local<v8::Object> bodyConfig = info.Data().As<v8::Object>();

        v8::Local<v8::Value> objectValue;
        if (!bodyConfig->Get(context, toV8String(isolate, "object"))
                 .ToLocal(&objectValue)) {
            reportError(context, tryCatch);
            return;
        }
        if (!objectValue->IsObject()) {
            reportError(context, tryCatch, "object should be an Object");
            return;
        }
        v8::Local<v8::Object> object = objectValue.As<v8::Object>();

        v8::Local<v8::Value> formatterValue;
        if (!bodyConfig->Get(context, toV8String(isolate, "formatter"))
                 .ToLocal(&formatterValue)) {
            reportError(context, tryCatch);
            return;
        }
        if (!formatterValue->IsObject()) {
            reportError(context, tryCatch, "formatter should be an Object");
            return;
        }
        v8::Local<v8::Object> formatter = formatterValue.As<v8::Object>();

        v8::Local<v8::Value> bodyValue;
        if (!formatter->Get(context, toV8String(isolate, "body"))
                 .ToLocal(&bodyValue)) {
            reportError(context, tryCatch);
            return;
        }
        if (!bodyValue->IsFunction()) {
            reportError(context, tryCatch, "body should be a Function");
            return;
        }
        v8::Local<v8::Function> bodyFunction = bodyValue.As<v8::Function>();

        v8::Local<v8::Value> configValue;
        if (!bodyConfig->Get(context, toV8String(isolate, "config"))
                 .ToLocal(&configValue)) {
            reportError(context, tryCatch);
            return;
        }

        v8::Local<v8::Value> sessionIdValue;
        if (!bodyConfig->Get(context, toV8String(isolate, "sessionId"))
                 .ToLocal(&sessionIdValue)) {
            reportError(context, tryCatch);
            return;
        }
        if (!sessionIdValue->IsInt32()) {
            reportError(context, tryCatch, "sessionId should be an Int32");
            return;
        }

        v8::Local<v8::Value> groupNameValue;
        if (!bodyConfig->Get(context, toV8String(isolate, "groupName"))
                 .ToLocal(&groupNameValue)) {
            reportError(context, tryCatch);
            return;
        }
        if (!groupNameValue->IsString()) {
            reportError(context, tryCatch, "groupName should be a string");
            return;
        }

        v8::Local<v8::Value> formattedValue;
        v8::Local<v8::Value> args[] = { object, configValue };
        if (!bodyFunction->Call(context, formatter, 2, args)
                 .ToLocal(&formattedValue)) {
            reportError(context, tryCatch);
            return;
        }
        if (!formattedValue->IsArray()) {
            reportError(context, tryCatch, "body should return an Array");
            return;
        }
        v8::Local<v8::Array> jsonML = formattedValue.As<v8::Array>();
        if (jsonML->Length() && !substituteObjectTags(sessionIdValue.As<v8::Int32>()->Value(), toProtocolString(isolate, groupNameValue.As<v8::String>()), context, jsonML, kMaxCustomPreviewDepth)) {
            return;
        }
        info.GetReturnValue().Set(jsonML);
    }
} // anonymous namespace

void generateCustomPreview(int sessionId, const String16& groupName,
    v8::Local<v8::Context> context,
    v8::Local<v8::Object> object,
    v8::MaybeLocal<v8::Value> maybeConfig, int maxDepth,
    std::unique_ptr<CustomPreview>* preview)
{
    v8::Isolate* isolate = context->GetIsolate();
    v8::MicrotasksScope microtasksScope(isolate,
        v8::MicrotasksScope::kDoNotRunMicrotasks);
    v8::TryCatch tryCatch(isolate);

    v8::Local<v8::Value> configValue;
    if (!maybeConfig.ToLocal(&configValue))
        configValue = v8::Undefined(isolate);

    v8::Local<v8::Object> global = context->Global();
    v8::Local<v8::Value> formattersValue;
    if (!global->Get(context, toV8String(isolate, "devtoolsFormatters"))
             .ToLocal(&formattersValue)) {
        reportError(context, tryCatch);
        return;
    }
    if (!formattersValue->IsArray())
        return;
    v8::Local<v8::Array> formatters = formattersValue.As<v8::Array>();
    v8::Local<v8::String> headerLiteral = toV8String(isolate, "header");
    v8::Local<v8::String> hasBodyLiteral = toV8String(isolate, "hasBody");
    for (uint32_t i = 0; i < formatters->Length(); ++i) {
        v8::Local<v8::Value> formatterValue;
        if (!formatters->Get(context, i).ToLocal(&formatterValue)) {
            reportError(context, tryCatch);
            return;
        }
        if (!formatterValue->IsObject()) {
            reportError(context, tryCatch, "formatter should be an Object");
            return;
        }
        v8::Local<v8::Object> formatter = formatterValue.As<v8::Object>();

        v8::Local<v8::Value> headerValue;
        if (!formatter->Get(context, headerLiteral).ToLocal(&headerValue)) {
            reportError(context, tryCatch);
            return;
        }
        if (!headerValue->IsFunction()) {
            reportError(context, tryCatch, "header should be a Function");
            return;
        }
        v8::Local<v8::Function> headerFunction = headerValue.As<v8::Function>();

        v8::Local<v8::Value> formattedValue;
        v8::Local<v8::Value> args[] = { object, configValue };
        if (!headerFunction->Call(context, formatter, 2, args)
                 .ToLocal(&formattedValue)) {
            reportError(context, tryCatch);
            return;
        }
        if (!formattedValue->IsArray())
            continue;
        v8::Local<v8::Array> jsonML = formattedValue.As<v8::Array>();

        v8::Local<v8::Value> hasBodyFunctionValue;
        if (!formatter->Get(context, hasBodyLiteral)
                 .ToLocal(&hasBodyFunctionValue)) {
            reportError(context, tryCatch);
            return;
        }
        if (!hasBodyFunctionValue->IsFunction())
            continue;
        v8::Local<v8::Function> hasBodyFunction = hasBodyFunctionValue.As<v8::Function>();
        v8::Local<v8::Value> hasBodyValue;
        if (!hasBodyFunction->Call(context, formatter, 2, args)
                 .ToLocal(&hasBodyValue)) {
            reportError(context, tryCatch);
            return;
        }
        bool hasBody = hasBodyValue->ToBoolean(isolate)->Value();

        if (jsonML->Length() && !substituteObjectTags(sessionId, groupName, context, jsonML, maxDepth)) {
            return;
        }

        v8::Local<v8::String> header;
        if (!v8::JSON::Stringify(context, jsonML).ToLocal(&header)) {
            reportError(context, tryCatch);
            return;
        }

        v8::Local<v8::Function> bodyFunction;
        if (hasBody) {
            v8::Local<v8::Object> bodyConfig = v8::Object::New(isolate);
            if (bodyConfig
                    ->CreateDataProperty(context, toV8String(isolate, "sessionId"),
                        v8::Integer::New(isolate, sessionId))
                    .IsNothing()) {
                reportError(context, tryCatch);
                return;
            }
            if (bodyConfig
                    ->CreateDataProperty(context, toV8String(isolate, "formatter"),
                        formatter)
                    .IsNothing()) {
                reportError(context, tryCatch);
                return;
            }
            if (bodyConfig
                    ->CreateDataProperty(context, toV8String(isolate, "groupName"),
                        toV8String(isolate, groupName))
                    .IsNothing()) {
                reportError(context, tryCatch);
                return;
            }
            if (bodyConfig
                    ->CreateDataProperty(context, toV8String(isolate, "config"),
                        configValue)
                    .IsNothing()) {
                reportError(context, tryCatch);
                return;
            }
            if (bodyConfig
                    ->CreateDataProperty(context, toV8String(isolate, "object"),
                        object)
                    .IsNothing()) {
                reportError(context, tryCatch);
                return;
            }
            if (!v8::Function::New(context, bodyCallback, bodyConfig)
                     .ToLocal(&bodyFunction)) {
                reportError(context, tryCatch);
                return;
            }
        }
        *preview = CustomPreview::create()
                       .setHeader(toProtocolString(isolate, header))
                       .build();
        if (!bodyFunction.IsEmpty()) {
            InjectedScript* injectedScript = getInjectedScript(context, sessionId);
            if (!injectedScript) {
                reportError(context, tryCatch, "cannot find context with specified id");
                return;
            }
            (*preview)->setBodyGetterId(
                injectedScript->bindObject(bodyFunction, groupName));
        }
        return;
    }
}
} // namespace v8_inspector
